package edu.uci.ics.ai.maze.controllers;

import edu.uci.ics.ai.maze.models.GameModel;
import edu.uci.ics.ai.maze.models.Vertex;
import edu.uci.ics.ai.maze.models.SearchAlgorithm;
import edu.uci.ics.ai.maze.models.SearchResult;
import edu.uci.ics.ai.maze.utils.MazeUtils;
import edu.uci.ics.ai.maze.utils.Throwables;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Observable;
import java.util.logging.Logger;

public class SearchEngine extends Observable {

  private static final Logger logger = Logger.getLogger(SearchEngine.class.getName());

  private final List<SearchAlgorithm> algorithms;
  private final GameModel model;
  private Thread searchThread;
  
  public SearchEngine(GameModel model) {
    this.model = model;
    this.algorithms = new ArrayList<SearchAlgorithm>();
  }

  /**
   * Returns an unmodifiable list of algorithms registered in the SearchEngine
   * @return the list of SearchAlgorithms
   */
  public List<SearchAlgorithm> getAlgorithms() {
    return Collections.unmodifiableList(this.algorithms);
  }

  /**
   * Registers the given algorithm with the SearchEngine
   * @param algorithm the algorithm to register
   */
  public void registerAlgorithm(SearchAlgorithm algorithm) {
    algorithms.add(algorithm);
  }

  /**
   * Removes the provided SearchEngine
   * @param algorithmToRemove
   * @return
   */
  public boolean remove(SearchAlgorithm algorithmToRemove) {
    return algorithms.remove(algorithmToRemove);
  }

  public void removeAll() {
    algorithms.clear();
  }

  public boolean removeAll(Collection<SearchAlgorithm> algorithmsToRemove) {
    return algorithms.removeAll(algorithmsToRemove);
  }

  /**
   * Attempts to find a path from start to end using the provided algorithm
   *
   * The search process is run in its own thread so that the application does
   * not become unresponsive.  Observers of SearchEngine are notified when
   * the search process completes.
   *
   * @param algorithm
   */
  public void search(final SearchAlgorithm algorithm) {
    if (model.getMaze() == null) {
      System.out.println("Warning: no Maze loaded.  Search will not execute.");
      return;
    }

    Runnable searchProcess = new Runnable() {

      @Override
      public void run() {

        model.setAlgorithm(algorithm);
        model.getMaze().clearCache();
        try {
          long start = System.nanoTime();
          List<Vertex> path = model.getAlgorithm().calculateReturnPath(model.getContext());
          long stop = System.nanoTime();

          long searchTime = stop - start;
          model.setSearchResult(
                  new SearchResult(model.getAlgorithm(), path, searchTime, computePathCost(path)));

          SearchEngine.this.setChanged();
          model.notifyObservers(GameModel.Observations.UPDATE_PATH);

        } catch (Exception e) {
          logger.severe(Throwables.stackTraceToString(e));
        } finally {
          SearchEngine.this.notifyObservers(Observations.SEARCH_ENDED);
        }
      }
    };

    searchThread = new Thread(searchProcess, algorithm.getName() + " Solver Thread");
    SearchEngine.this.setChanged();
    SearchEngine.this.notifyObservers(Observations.SEARCH_STARTED);
    searchThread.start();
  }

  @SuppressWarnings("deprecation")
  public void stopSearch() {
    if (searchThread != null) {
      // Note: the stop method is deprecated typically this should set
      // a stop variable to be read by the executing script; however,
      // since students provide the search engine, we can not guaruntee
      // that this will be correctly responded to, so we will force the
      // stop.
      searchThread.stop();
      setChanged();
      notifyObservers(Observations.SEARCH_ENDED);
    }
  }

  public double computePathCost(List<Vertex> path) {
    double cost = 0;
    if (path != null) {
      for (int i = 0; i < path.size() - 1; i++) {
        cost += MazeUtils.euclideanDistance(path.get(i), path.get(i + 1));
      }

      int decPart = (int) ((cost - (int) cost) * 1000);
      int thirdDigit = decPart % 10;
      decPart /= 10;
      if (thirdDigit >= 5) {
        decPart++;
      }
      if (decPart < 100) {
        cost = ((int) cost) + (double) decPart / 100;
      } else {
        cost = ((int) cost) + 1;
      }
    }
    return cost;
  }

  @SuppressWarnings("PublicInnerClass")
  public static enum Observations {

    SEARCH_STARTED,
    SEARCH_ENDED
  }
}
